
# Copyright (c) 2014, Franklin Brauning V.  All rights reserved.
# 
#    El desarrollo inicial de esta utilidad fue producido
#    por Franklin Brauning <brauningbar@gmail.com>
# 
#    Si consideras que este codigo puede serte util, eres libre
#    de usarlo a conveniencia, pero no olvides mantener la licencia
#    y dar credito al autor.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 
#   * Redistributions of source code must retain the above copyright notice, this
#     list of conditions and the following disclaimer.
# 
#   * Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions and the following disclaimer in the documentation
#     and/or other materials provided with the distribution.
# 
#   * Neither the name of the author nor the names of its contributors may
#     be used to endorse or promote products derived from this software without
#     specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
# OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

from django.db import models
from .base import *
from django.core.exceptions import ObjectDoesNotExist
from django.db.transaction import atomic

class FossilException(AbstractException):
	''' exception for fossil models '''
	pass

class InvalidFossil(AbstractException):
	''' this exception will be raised by models based on fossil
	to indicate that the involved versions configurations do not
	permit to succed
	'''
	pass

def show_fossil(func):
  ''' composable __str__ decorator to show fossil models info '''
  def w(self):
  	s,ats = func(self)
  	curat = []
  	#ats.append("%s-%d-%s" % (str(self.prevcopy), self.id, str(self.nextcopy)))
  	ats.append('rev'+str(self.revno))
  	if self.current:
  		ats.append("current")
  	if self.prevcopy:
  		ats.append("undoable")
  	if self.nextcopy:
  		ats.append("redoable")
  	else:
  		ats.append("tip")
  	return s,ats
  return w

FIRST_REVNO = 1

class fossil(abstractmodel):
	''' this class enriches a model to store creation, modification
	and deletions (or any two of them...) as the versions of the
	same *thing*.

	* Every node can be rolledback in both directions (undo/redo).

	* Related nodes pointing at the rolledback nodes will always
	point to the current version

	* Versions can be traversed in both directions (if any)

	* Keep the history simple. Garbage collect when possible
	and keep linear

	* Other tidy-models use this to keep their structures consistent

	* Dont hesitate to use this. It coulnt be simpler
	'''
	prevcopy = models.IntegerField(null=True, editable=False)
	nextcopy = models.IntegerField(null=True, editable=False)
	current = models.BooleanField(default=True, editable=False)
	revno = models.IntegerField(default=FIRST_REVNO-1, editable=False)

	class Meta:
		abstract = True

	# XXX
	# Modos de operacion al bifurcar desde una version que no es tip
	#
	# rebase append garbage tip
	#   Tomar la punta del tramo que se va a eliminar
	#   y copiar esa punta como hija de la nueva version
	# rebase append garbage
	#   Dejar el tramo que se va a eliminar como descendientes
	#   de la nueva version
	#
	# rebase over tip
	#   Recolectar los nodos del tramo que se va a eliminar y dejar
	#   solo su punta, sobre ella ubicar la nueva version
	# rebase over garbage
	#   Ubicar la nueva version como hija del trama que se eliminaria
	#
	@atomic
	def redo(self, updateobj=True, old=False):
		''' redo.

		Advance to the next version of this *register*

		This proceeds if .undo has been done previous to this
		call.  Otherwise do nothing. It can be done as many
		times as versions available

		Returns the obtained version.

		With updateobj=True (default) updates the current object

		With old=True return the current version (the soon
		no-more-active version) updated from the updated
		versioning structure
		'''
		if self.nextcopy:
			self.trace("fossil.redo")
			nextobj = self.getnextcopy()

			# actualizamos los enlaces
			self._swapids(nextobj, self)
			self._linkfossilobjs(self, nextobj)
			if nextobj.nextcopy:
				nextnextobj = nextobj.getnextcopy()
				self._linkfossilobjs(nextobj, nextnextobj)
				nextnextobj.rawsave()
			if self.prevcopy:
				prevobj = self.getprevcopy()
				self._linkfossilobjs(prevobj, self)
				prevobj.rawsave()

			nextobj.rawsave()
			self.rawsave()
			if old:
				# implica updateobj=True
				return self
			# actualizamos actual objeto con los datos del registro
			# "recuperado"
			if updateobj:
				self.__dict__ = nextobj.__dict__
				self.modified = False
			return nextobj
		return self

	@atomic
	def undo(self, updateobj=True, old=False):
		''' redo.

		Rollback to the previous version of this *register*

		This proceeds if .save has been done previous to this call...
		Otherwise do nothing. It can be done as many times as
		versions available

		Returns the obtained version.

		With updateobj=True (default) updates the current object

		With old=True return the current version (the soon
		no-more-active version) updated from the updated
		versioning structure
		'''
		if self.prevcopy:
			self.trace("fossil.undo")
			prevobj = self.getprevcopy()

			# actualizamos los enlaces
			self._swapids(prevobj, self)
			self._linkfossilobjs(prevobj, self)
			if prevobj.prevcopy:
				prevprevobj = prevobj.getprevcopy()
				self._linkfossilobjs(prevprevobj, prevobj)
				prevprevobj.rawsave()
			if self.nextcopy:
				nextobj = self.getnextcopy()
				self._linkfossilobjs(self, nextobj)
				nextobj.rawsave()

			prevobj.rawsave()
			self.rawsave()
			if old:
				return self
			# actualizamos actual objeto con los datos del registro
			# "recuperado"
			if updateobj:
				self.__dict__ = prevobj.__dict__
				self.modified = False
			return prevobj
		return self

	def getprevcopy(self):
		''' get a copy of the previous version but dont modify
		the *version alignment*.

		After this you could modify and .save to branch from
		this version. If you do this, the rolledback versions
		will get trimed of the structure (gc)
		'''
		return self.__class__.objects.get(id=self.prevcopy)

	def getnextcopy(self):
		''' get a copy of the next version but dont modify
		the *version alignment*.

		After this you could modify and .save to branch from
		this version. If you do this, and there are even more
		next versions, these will get trimed of the structure (gc)
		'''
		return self.__class__.objects.get(id=self.nextcopy)

	def foreach_forward(self, use_self=True):
		obj = self
		if use_self:
			yield obj
		while obj.nextcopy:
			obj = obj.getnextcopy()
			yield obj

	def foreach_rewind(self, use_self=True):
		obj = self
		if use_self:
			yield obj
		while obj.prevcopy:
			obj = obj.getprevcopy()
			yield obj

	def forward(self, visitor=None):
		''' traverse forward over the versions of a register
		and return the last visited node.

		If a visitor funcion is provided, control when to exit
		returning True
		'''
		obj = self
		while obj.nextcopy:
			obj = obj.getnextcopy()
			if visitor and visitor(obj):
				break
		return obj

	def rewind(self, visitor=None):
		''' traverse backwards over the versions of a register
		and return the last visited node.

		If a visitor funcion is provided, control when to exit
		returning True
		'''
		obj = self
		while obj.prevcopy:
			obj = obj.getprevcopy()
			if visitor and visitor(obj):
				break
		return obj

	def _forward_ids(self):
		pks = []
		self.forward(visitor=lambda n:pks.append(n.id))
		return pks

	def _rewind_ids(self):
		pks = []
		self.rewind(visitor=lambda n:pks.append(n.id))
		return pks

	def _swapids(self, obja, objb, presave=False, save=False, current=True):
		if current:
			self.microtrace("fossil._swapids/current")
			obja.current, objb.current = objb.current, obja.current
		if presave:
			self.microtrace("fossil._swapids/presave")
			super(fossil, obja).save()
		obja.id, objb.id = objb.id, obja.id
		if save:
			self.microtrace("fossil._swapids/save")
			obja.rawsave()
			objb.rawsave()

	def _linkfossilids(self, a, b, **kw):
		self._linkfossilobjs(self.__class__.objects.get(id=a),
		  self.__class__.objects.get(id=b), **kw)

	def _linkfossilobjs(self, obja, objb, save=False):
		obja.nextcopy = objb.id
		objb.prevcopy = obja.id
		if save:
			self.microtrace("fossil._linkfossilobjs/save")
			obja.rawsave()
			objb.rawsave()

	# precontrato
	def ensurenocurrent(self):
		''' notify fossil that the soon to be saved object wont
		be marked as the current.
		Use this instead of accessing this class attributes
		'''
		self._current = False

	# one-time check interno
	def _markcurrent(self):
		c = getattr(self, '_current', True)
		self._current = True
		return c

	@atomic
	def save(self, *a, **kw):
		''' save fossil '''
		self.revno += 1
		try:
			oldhead = self.__class__.objects.get(id=self.id)
		except ObjectDoesNotExist:
			return super(fossil, self).save(*a, **kw)

		# guardamos la modificacion bajo un nuevo registro (siempre)
		# y lo agregamos a la cabeza del historial (usaremos self
		# como destino)
		self.trace("fossil.save")
		self.id = None
		# aprovechamos de asumir que el actual objeto era 'current'
		oldhead.current, self.current = self._markcurrent(), False
		# ahora swapeamos las ids para dejar a la cabeza la id antigua
		self._swapids(self, oldhead, presave=True, save=True)

		# ahora que los objetos estan en posicion reajustar los enlaces
		if oldhead.prevcopy:
			prevcopy = oldhead.getprevcopy()
			self._linkfossilobjs(prevcopy, oldhead)
			prevcopy.rawsave()
		self._linkfossilobjs(oldhead, self)
		oldhead.rawsave()

		# recoleccion de basura:
		# se esta creando una nueva cabeza por lo tanto
		# las referencias hacia adelante ya no son vigentes
		#
		if self.nextcopy:
			ids_basura = self._forward_ids()
			self.microtrace("fossil.save:recolectando basura (%d items)" % len(ids_basura))
			self.nextcopy = None
			self.__class__.objects.filter(id__in=ids_basura).delete()
		super(fossil, self).save()

		# consecuencias de esta implementacion.
		# Si desde el item 'current' (y tip tambien) hemos retrocedido en
		# la cadena (recorrer la estructura, no undo) y en alguno de estas
		# previas copias guardamos una nueva copia,
		# todos los items saltados se eliminan como parte de la recoleccion
		# de basura, quedando el nuevo item creado como unico 'current'
		# (y tip tambien)
		#
		# ahora, si nos movemos hacia adelante desde el item 'current' y guardamos
		# una nueva copia desde ese item, el item 'current' inicial se mantiene
		# con ese status, pero el recientemente creado item tambien es 'current':
		# Entonces el item verdaderamente 'current' sera el que no tiene
		# descendientes 'current'
	def real_current(self):
		''' una consecuencia de la implementacion. Es que se asume
		que el objeto que el usuario esta manipulando, y desde donde
		se modifican y se crean nuevas versiones es el registro
		efectivamente current (ver explicacion del fenomeno en la
		implementacion de .save)
		Si demuestra ser un problema de seguridad o de corrupcion
		este metodo debera ser invocado regularmente para no viciar
		la base de datos '''
		lastcurrent = self.id if self.current else None
		def updatecurrent(n):
			if n.current:
				nonlocal lastcurrent
				lastcurrent = n.id
		self.forward(visitor=updatecurrent)
		if lastcurrent:
			if self.id == lastcurrent:
				return self
			else:
				return self.__class__.objects.get(id=lastcurrent)

		raise FossilException("real_current:se encontro un tramo de versiones sin current")


# This model provides primitives to extend fossil-like models
# to keep track of events that operate on several records at once
#
class linked_content(models.Model):
	class Meta:
		abstract = True
	prevlink = models.IntegerField(null=True, editable=False)
	nextlink = models.IntegerField(null=True, editable=False)

	def save(self):
		if not self._propagate_links():
			if getattr(self, 'prevlink', None):
				super(linked_content, self).__setattr__('prevlink', None)
			if getattr(self, 'nextlink', None):
				super(linked_content, self).__setattr__('nextlink', None)
		return super(linked_content, self).save()

	def _mark_propagate_links(self, status):
		self._mpropagate_links = status

	def _propagate_links(self):
		return getattr(self, '_mpropagate_links', True)

	def _link(self, a, b, lextreme=False, rextreme=False, clear=False):
		a.nextlink = b.id
		b.prevlink = a.id
		if lextreme:
			a.prevlink = -1
		elif clear:
			a.prevlink = None
		if rextreme:
			b.nextlink = -1
		elif clear:
			b.nextlink = None


	def islinked(self):
		''' check if the current node state is related to other
		nodes.

		You should use this just for debugging
		'''
		return bool(self.prevlink or self.nextlink)

	def getprevlink(self):
		return self.__class__.objects.get(id=self.prevlink)

	def getnextlink(self):
		return self.__class__.objects.get(id=self.nextlink)

	def forward_links(self, reducer=None, fault_handler=None, check=True):
		obj, old = self, self
		while obj.nextlink and obj.nextlink != -1:
			obj = obj.getnextlink()
			if reducer:
				obj = reducer(obj)
				if not obj:
					break
			if check:
				if obj.prevlink != old.id:
					return fault_handler(old, obj)
				old = obj
		if check and self.id != obj.id and obj.nextlink != -1:
			return fault_handler(old, obj)
		return obj

	def rewind_links(self, reducer=None, fault_handler=None, check=True):
		obj, old = self, self
		while obj.prevlink and obj.prevlink != -1:
			obj = obj.getprevlink()
			if reducer:
				obj = reducer(obj)
				if not obj:
					break
			if check:
				if obj.nextlink != old.id:
					return fault_handler(old, obj)
				old = obj
		if check and self.id != obj.id and obj.prevlink != -1:
			return fault_handler(old, obj)
		return obj

	@atomic
	def undo(self, **kw):
		''' undo this node keeping consistent the overall structure.

		blah... just undo
		'''
		if not self.islinked():
			return super(linked_content, self).undo(**kw)
		self.trace("linked_content.undo")

		# the handler functions below (rewindhandler/forwardhandler)
		# get triggered when the linked content is not reachable.
		# Those should raise an exception to imply db rollback.
		# We will assume no transaction support and will cleanup
		# modified registers.
		#
		def rewindhandler(old, cur):
			super(linked_content, old).redo(updateobj=False)
			old.forward_links(
			  reducer=lambda n:super(linked_content, n).redo(updateobj=False),
			  check=False)
			raise InvalidFossil("linked_content.undo/rewindhandler:desface de nodos (old:%d, cur:%d)" % \
			                      (old.id, cur.id))

		def forwardhandler(old, cur):
			super(linked_content, old).redo(updateobj=False)
			old.rewind_links(
			  reducer=lambda n:super(linked_content, n).redo(updateobj=False),
			  check=False)
			raise InvalidFossil("linked_content.undo/forwardhandler:desface de nodos (old:%d, cur:%d)" % \
			                      (old.id, cur.id))

		self._mark_propagate_links(True)
		self.rewind_links(
		  reducer=lambda n:super(linked_content, n).undo(old=True),
		  fault_handler=rewindhandler)
		self.forward_links(
		  reducer=lambda n:super(linked_content, n).undo(old=True),
		  fault_handler=forwardhandler)
		s = super(linked_content, self).undo(**kw)
		self._mark_propagate_links(False)
		return s

	# operativamente correcto pero la deteccion de desface es
	# ineficiente
	# ...de todos modos, los enlaces (al momento de escribir este parrafo)
	# contienen solo tres nodos
	#
	# se procede en tandem a hacer redo y verificar que los enlaces
	# se vayan recreando. Si algun nodo no calza con el tramo enlazado
	# quiere decir que hay un desface (la transaccion se deshace)
	#
	# Notar que el redo requiere un flag explicito que indique cuales
	# son los nodos extremo (esto no puede ser deducido), por eso
	# a los nodos extremo se les marca -1 como enlace canario en el sentido
	# de recorrido a considerar
	#
	@atomic
	def redo(self, **kw):
		''' redo this node keeping consistent the overall structure.

		blah... just redo
		'''
		self._mark_propagate_links(True)
		s = super(linked_content, self).redo(**kw)
		if s.islinked():
			self.trace("linked_content.redo")

			# notar que los fault_handler a su vez vuelven a recorrer en sentido
			# inverso la estructura con redo parcialmente aplicado para deshacerlo
			# pero no consideran un fault_handler porque estamos seguros que
			# su recorrido no producira fallos
			#

			# the handler functions below (rewindhandler/forwardhandler)
			# get triggered when the linked content is not reachable.
			# Those should raise an exception to imply db rollback.
			# We will assume no transaction support and will cleanup
			# modified registers.
			#
			def rewindhandler(old, cur):
				super(linked_content, old).undo(updateobj=False)
				old.forward_links(
				  reducer=lambda n:super(linked_content, n).undo(old=True),
				  check=False)
				raise InvalidFossil("linked_content.redo/rewindhandler:desface de nodos (old:%d, cur:%d)" % \
									  (old.id, cur.id))

			def forwardhandler(old, cur):
				super(linked_content, old).undo(updateobj=False)
				old.rewind_links(
				  reducer=lambda n:super(linked_content, n).undo(old=True),
				  check=False)
				raise InvalidFossil("linked_content.redo/forwardhandler:desface de nodos (old:%d, cur:%d)" % \
									  (old.id, cur.id))

			s.rewind_links(
			  reducer=lambda n:super(linked_content, n).redo(updateobj=False),
			  fault_handler=rewindhandler)
			s.forward_links(
			  reducer=lambda n:super(linked_content, n).redo(updateobj=False),
			  fault_handler=forwardhandler)
		else:
			self.trace("not linked")
		self._mark_propagate_links(False)
		s._mark_propagate_links(False)
		return s

